package org.unidata.mdm.rest.v1.data.service.lob;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.function.Supplier;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.apache.commons.lang3.StringUtils;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.apache.cxf.jaxrs.ext.multipart.ContentDisposition;
import org.apache.cxf.jaxrs.ext.multipart.Multipart;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.core.context.DeleteLargeObjectContext;
import org.unidata.mdm.core.context.FetchLargeObjectContext;
import org.unidata.mdm.core.context.UpsertLargeObjectContext;
import org.unidata.mdm.core.dto.LargeObjectResult;
import org.unidata.mdm.core.service.LargeObjectsService;
import org.unidata.mdm.core.type.lob.LargeObjectAcceptance;
import org.unidata.mdm.core.util.FileUtils;
import org.unidata.mdm.core.util.LargeObjectUtils;
import org.unidata.mdm.data.exception.DataProcessingException;
import org.unidata.mdm.rest.system.ro.DetailedErrorResponseRO;
import org.unidata.mdm.rest.system.util.RestConstants;
import org.unidata.mdm.rest.v1.data.converter.LargeObjectToRestLargeObjectConverter;
import org.unidata.mdm.rest.v1.data.ro.lob.DeleteLobRequestRO;
import org.unidata.mdm.rest.v1.data.ro.lob.DeleteLobResultRO;
import org.unidata.mdm.rest.v1.data.ro.lob.FetchLobRequestRO;
import org.unidata.mdm.rest.v1.data.ro.lob.UploadResultRO;
import org.unidata.mdm.rest.v1.data.service.AbstractDataRestService;
import org.unidata.mdm.system.exception.ExceptionId;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;

/**
 * Large object service
 *
 * @author Alexandr Serov
 * @since 14.10.2020
 **/
@Path("lob")
public class LargeObjectRestService extends AbstractDataRestService {

    private static final String LOB_TAG = "lob";

    private static final String CONTENT_TYPE = "Content-Type";

    private static final String CONTENT_DISPOSITION = "Content-Disposition";

    public static final ExceptionId EX_DATA_INVALID_CLOB_OBJECT = new ExceptionId("EX_DATA_INVALID_CLOB_OBJECT", "");

    @Autowired
    private LargeObjectsService largeObjectsService;

    /**
     * Saves binary large object.
     *
     * @param blobId golden record id
     * @param attr attribute
     * @param attachment attachment object
     * @return ok/nok
     */
    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Path("/" + RestConstants.PATH_PARAM_BLOB + "/{" + RestConstants.DATA_PARAM_ATTR + "}" + "{p:/?}{" + RestConstants.DATA_PARAM_ID + ": (([a-zA-Z0-9\\-]{36})?)}")
    @Operation(description = "Saves binary large object.",
        requestBody = @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = Attachment.class))),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "400"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = UploadResultRO.class)), responseCode = "200")
        }, tags = LOB_TAG
    )
    public UploadResultRO uploadBlobData(
        @Parameter(description = "Attribute name", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ATTR) String attr,
        @Parameter(description = "Object ID", in = ParameterIn.PATH, required = false) @PathParam(RestConstants.DATA_PARAM_ID) String blobId,
        @Multipart(value = RestConstants.DATA_PARAM_FILE) Attachment attachment) {
        return executeSaveLargeObject(blobId, attr, attachment, true);
    }

    /**
     * Saves character large object.
     *
     * @param clobId golden record id
     * @param attr attribute
     * @param attachment attachment object
     * @return ok/nok
     */
    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Path("/" + RestConstants.PATH_PARAM_CLOB + "/{" + RestConstants.DATA_PARAM_ATTR + "}" + "{p:/?}{" + RestConstants.DATA_PARAM_ID + ": (([a-zA-Z0-9\\-]{36})?)}")
    @Operation(description = "Saves character large object.", responses = {
        @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "400"),
        @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500"),
        @ApiResponse(content = @Content(schema = @Schema(implementation = UploadResultRO.class)), responseCode = "200")
    }, tags = LOB_TAG)
    public UploadResultRO uploadClobData(
        @Parameter(description = "Attribute name", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ATTR) String attr,
        @Parameter(description = "Object ID", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String clobId,
        @Multipart(value = RestConstants.DATA_PARAM_FILE) Attachment attachment) {
        return executeSaveLargeObject(clobId, attr, attachment, false);
    }

    /**
     * Gets CLOB data associated with an attribute of a golden record.
     *
     * @param clobId LOB object id.
     * @return byte stream
     */
    @GET
    @Path("/" + RestConstants.PATH_PARAM_CLOB + "/{" + RestConstants.DATA_PARAM_ID + "}")
    @Produces("text/plain")
    @Operation(description = "Gets CLOB data associated with an attribute of a golden record.", responses = {
        @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "400"),
        @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500")
    }, tags = LOB_TAG)
    public Response fetchClobData(@Parameter(description = "Clob ID", in = ParameterIn.PATH)
                                  @PathParam(RestConstants.DATA_PARAM_ID) String clobId) {
        return executeFetchLargeObject(new FetchLobRequestRO(clobId, false));
    }

    /**
     * Gets BLOB data associated with an attribute of a golden record.
     *
     * @param blobId LOB object id.
     * @return byte stream
     */
    @GET
    @Path("/" + RestConstants.PATH_PARAM_BLOB + "/{" + RestConstants.DATA_PARAM_ID + "}")
    @Operation(description = "Gets BLOB data associated with an attribute of a golden record.", responses = {
        @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "400"),
        @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500")
    }, tags = LOB_TAG)
    public Response fetchBlobData(@Parameter(description = "Blob ID", in = ParameterIn.PATH)
                                  @PathParam(RestConstants.DATA_PARAM_ID) String blobId) {
        return executeFetchLargeObject(new FetchLobRequestRO(blobId, true));
    }

    /**
     * Deletes golden blob data.
     *
     * @param blobId the id
     * @param attr the attribute
     * @return ok/nok
     */
    @DELETE
    @Path("/" + RestConstants.PATH_PARAM_BLOB + "/{" + RestConstants.DATA_PARAM_ID + "}")
    @Operation(
        description = "Удалить двоичные данные записи для ID реестра и имени атрибута.",
        method = DELETE_REQUEST,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "400"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DeleteLobResultRO.class)), responseCode = "200")
        }, tags = LOB_TAG
    )
    public DeleteLobResultRO deleteBlobData(
        @Parameter(description = "ID сущности", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String blobId,
        @Parameter(description = "Имя аттрибута", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ATTR) String attr) {
        return executeDeleteLargeObject(new DeleteLobRequestRO(blobId, attr, true));
    }

    /**
     * Deletes golden CLOB data.
     *
     * @param clobId the id
     * @param attr the attribute
     * @return ok/nok
     */
    @DELETE
    @Path("/" + RestConstants.PATH_PARAM_CLOB + "/{" + RestConstants.DATA_PARAM_ID + "}")
    @Operation(
        description = "Удалить символьные данные записи для ID реестра и имени атрибута.",
        method = DELETE_REQUEST,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "400"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DeleteLobResultRO.class)), responseCode = "200")
        }, tags = LOB_TAG
    )
    public DeleteLobResultRO deleteClobData(
        @Parameter(description = "ID сущности", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String clobId,
        @Parameter(description = "Имя аттрибута", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ATTR) String attr) {
        return executeDeleteLargeObject(new DeleteLobRequestRO(clobId, attr, false));
    }


    private Response executeFetchLargeObject(FetchLobRequestRO req) {
        Objects.requireNonNull(req, "FetchLobRequestRO can't be null");
        final LargeObjectResult result = largeObjectsService.fetchLargeObject(FetchLargeObjectContext.builder()
            .largeObjectId(req.getId())
            .binary(req.isBinary())
            .build());
        String fileName = StringUtils.isBlank(result.getFileName()) ? StringUtils.EMPTY : FileUtils.urlEncode(result.getFileName());
        Response.ResponseBuilder response = Response.ok(LargeObjectUtils.createStreamingOutputForLargeObject(result))
            .encoding(StandardCharsets.UTF_8.name())
            .header(CONTENT_DISPOSITION, String.format("attachment; filename=%s; filename*=UTF-8'' %s", fileName, fileName));
        if (StringUtils.isNotBlank(result.getMimeType())) {
            response.header(CONTENT_TYPE, result.getMimeType());
        } else if (req.isBinary()) {
            response.header(CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_TYPE);
        } else {
            response.header(CONTENT_TYPE, MediaType.TEXT_PLAIN_TYPE);
        }
        return response.build();
    }


    private UploadResultRO executeSaveLargeObject(String lobId, String attribute, Attachment attachment, boolean binary) {
        ContentDisposition contentDisposition = attachment.getContentDisposition();
        MediaType mediaType = attachment.getContentType();
        if (binary || isClobMediaType(mediaType)) {
            UploadResultRO result = new UploadResultRO();
            LargeObjectResult largeObject = largeObjectsService.saveLargeObject(UpsertLargeObjectContext.builder()
                .tags("attribute:" + StringUtils.trim(attribute))
                .largeObjectId(lobId)
                .binary(binary)
                .acceptance(LargeObjectAcceptance.PENDING)
                .input(new AttachmentStreamSource(attachment))
                .filename(contentDisposition.getParameter(RestConstants.DATA_PARAM_FILENAME))
                .mimeType(new StringBuilder()
                        .append(mediaType.getType())
                        .append('/')
                        .append(mediaType.getSubtype())
                        .toString())
                .build());
            result.setLargeObjectRO(LargeObjectToRestLargeObjectConverter.convert(largeObject));
            return result;
        } else {
            throw new DataProcessingException("The media type [{}] is not allowed for character objects.", EX_DATA_INVALID_CLOB_OBJECT, attachment.getContentType().toString());
        }
    }

    private DeleteLobResultRO executeDeleteLargeObject(DeleteLobRequestRO req) {
        Objects.requireNonNull(req, "DeleteLobRequestRO can't be null");
        DeleteLobResultRO result = new DeleteLobResultRO();
        boolean deleted = largeObjectsService.deleteLargeObject(DeleteLargeObjectContext.builder()
            .largeObjectId(req.getId())
            .tags("attribute:" + StringUtils.trim(req.getAttribute()))
            .binary(req.isBinary())
            .build());
        result.setDeleted(deleted);
        return null;
    }

    private boolean isClobMediaType(MediaType mediaType) {
        return "text".equals(mediaType.getType());
    }


    /**
     * Attachment input stream supplier
     */
    private static class AttachmentStreamSource implements Supplier<InputStream> {

        private final Attachment attachment;

        private AttachmentStreamSource(Attachment attachment) {
            this.attachment = attachment;
        }

        @Override
        public InputStream get() {
            InputStream inputStream = attachment.getObject(InputStream.class);
            if (inputStream == null) {
                throw new RuntimeException("InputStream not preset in attachment");
            }
            return inputStream;
        }
    }

}

